# ExpandableSQLAppender.java

    package org.codefilarete.stalactite.query.builder;

    import javax.annotation.Nullable;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.function.Consumer;

    import org.codefilarete.stalactite.query.model.ConditionalOperator;
    import org.codefilarete.stalactite.query.api.Fromable;
    import org.codefilarete.stalactite.query.model.Placeholder;
    import org.codefilarete.stalactite.query.api.Selectable;
    import org.codefilarete.stalactite.query.model.ValuedVariable;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.stalactite.sql.statement.ExpandableSQL;
    import org.codefilarete.stalactite.sql.statement.ExpandableStatement;
    import org.codefilarete.stalactite.sql.statement.PreparedSQL;
    import org.codefilarete.stalactite.sql.statement.SQLParameterParser.ParsedSQL;
    import org.codefilarete.stalactite.sql.statement.SQLStatement.BindingException;
    import org.codefilarete.stalactite.sql.statement.binder.ColumnBinderRegistry;
    import org.codefilarete.stalactite.sql.statement.binder.ParameterBinder;
    import org.codefilarete.stalactite.sql.statement.binder.PreparedStatementWriter;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.trace.MutableInt;

    /**
     * {@link SQLAppender} that can handle not-yet-set values from {@link ConditionalOperator}s as well as
     * already-set ones.
     * - Set values are those made of {@link ValuedVariable} and a numeric (incremental) placeholder is affected to them
     * - Not-yet-set values are placeholder ones made of {@link Placeholder} and the variable name is affected to them
     * Final result can be converted to a {@link PreparedSQL} with given values, see {@link #toPreparedSQL(Map)}
     *
     * @see #toPreparedSQL(Map)
     * @author Guillaume Mary
     */
    public class ExpandableSQLAppender implements SQLAppender {

    	/**
    	 * Used to store SQL snippets
    	 */
    	private final List<Object /* Placeholder, StringSQLAppender, or SubSQLAppender */> sqlSnippets = new ArrayList<>();
    	private final DMLNameProvider dmlNameProvider;

    	/**
    	 * Current "String" in which expressions are appended. The instance is put into the {@link ParsedSQL} instance as a sql snippet.
    	 * The reference changes as soon as a placeholder or a variable is added to {@link ExpandableSQLAppender}.
    	 */
    	private StringSQLAppender currentSQLSnippet;
    	private final ColumnBinderRegistry parameterBinderRegistry;
    	/**
    	 * Collected {@link ParameterBinder} per variable name
    	 */
    	private final Map<String, ParameterBinder> parameterBinders;
    	/**
    	 * Collected values per variable name
    	 */
    	private final Map<String, Object> values;
    	/**
    	 * Counter for unnamed variables : each time a raw value is appended to the {@link ExpandableSQLAppender} instance, this counter is incremented
    	 * and a variable name is created for it, simply made of the Integer as a String
    	 */
    	private final MutableInt paramCounter;

    	public ExpandableSQLAppender(ColumnBinderRegistry parameterBinderRegistry, DMLNameProvider dmlNameProvider) {
    		this.dmlNameProvider = dmlNameProvider;
    		this.parameterBinderRegistry = parameterBinderRegistry;
    		this.parameterBinders = new HashMap<>();
    		this.values = new HashMap<>();
    		this.paramCounter = new MutableInt();
    		initCurrentSqlSnippet();
    	}

    	/**
    	 * Private constructor for {@link #newSubPart(DMLNameProvider)}
    	 * @param dmlNameProvider
    	 * @param parameterBinderRegistry
    	 * @param parameterBinders
    	 * @param values
    	 * @param paramCounter
    	 */
    	private ExpandableSQLAppender(
    			DMLNameProvider dmlNameProvider,
    			ColumnBinderRegistry parameterBinderRegistry,
    			Map<String, ParameterBinder> parameterBinders,
    			Map<String, Object> values,
    			MutableInt paramCounter) {
    		this.dmlNameProvider = dmlNameProvider;
    		this.parameterBinderRegistry = parameterBinderRegistry;
    		this.parameterBinders = parameterBinders;
    		this.values = values;
    		this.paramCounter = paramCounter;
    		initCurrentSqlSnippet();
    	}

    	public List<Object> getSqlSnippets() {
    		return sqlSnippets;
    	}

    	public Map<String, Object> getValues() {
    		return values;
    	}

    	@Override
    	public SQLAppender cat(String s, String... ss) {
    		currentSQLSnippet.cat(s, ss);
    		return this;
    	}

    	@Override
    	public <V> SQLAppender catValue(@Nullable Selectable<V> column, Object value) {
    		ParameterBinder<?> parameterBinder;
    		if (column == null) {
    			parameterBinder = getParameterBinderFromRegistry(value);
    		} else if (column instanceof Column) {
    			parameterBinder = parameterBinderRegistry.getBinder((Column) column);
    		} else {
    			parameterBinder = parameterBinderRegistry.getBinder(column.getJavaType());
    		}
    		return catValue(value, parameterBinder);
    	}

    	@Override
    	public SQLAppender catValue(Object variable) {
    		if (variable instanceof ValuedVariable) {
    			Object value = ((ValuedVariable) variable).getValue();
    			if (value instanceof Selectable) {
    				// Columns are simply appended (no binder needed nor index increment)
    				currentSQLSnippet.catColumn((Selectable) value);
    			} else {
    				addPlaceholder(value, getParameterBinderFromRegistry(value));
    			}
    		} else if (variable instanceof Placeholder) {
    			addPlaceholder((Placeholder<?, ?>) variable);
    		} else {
    			addPlaceholder(variable, getParameterBinderFromRegistry(variable));
    		}
    		return this;
    	}

    	private ParameterBinder<?> getParameterBinderFromRegistry(Object value) {
    		ParameterBinder<?> parameterBinder;
    		if (value instanceof ValuedVariable) {
    			parameterBinder = getParameterBinderFromRegistry(((ValuedVariable) value).getValue());
    		} else if (value instanceof Placeholder) {
    			parameterBinder = parameterBinderRegistry.getBinder(((Placeholder) value).getValueType());
    		} else {
    			Class<?> binderType = value.getClass().isArray() ? value.getClass().getComponentType() : value.getClass();
    			parameterBinder = parameterBinderRegistry.getBinder(binderType);
    		}
    		return parameterBinder;
    	}

    	private SQLAppender catValue(Object value, ParameterBinder<?> binderSupplier) {
    		if (value instanceof ValuedVariable) {
    			Object innerValue = ((ValuedVariable) value).getValue();
    			if (innerValue instanceof Selectable) {
    				// Columns are simply appended (no binder needed nor index increment)
    				currentSQLSnippet.catColumn((Selectable) innerValue);
    			} else {
    				addPlaceholder(innerValue, binderSupplier);
    			}
    		} else if (value instanceof Placeholder) {
    			addPlaceholder((Placeholder<?, ?>) value);
    		} else {
    			addPlaceholder(value, binderSupplier);
    		}
    		return this;
    	}

    	private void addPlaceholder(Placeholder<?, ?> variable) {
    		sqlSnippets.add(variable);
    		parameterBinders.put(variable.getName(), parameterBinderRegistry.getBinder(variable.getValueType()));
    		initCurrentSqlSnippet();
    	}

    	private void addPlaceholder(Object value, ParameterBinder<?> binderSupplier) {
    		String paramName = String.valueOf(paramCounter.increment());
    		sqlSnippets.add(new Placeholder<>(paramName, binderSupplier.getColumnType()));
    		parameterBinders.put(paramName, binderSupplier);
    		values.put(paramName, value);
    		initCurrentSqlSnippet();
    	}

    	protected void initCurrentSqlSnippet() {
    		currentSQLSnippet = new StringSQLAppender(dmlNameProvider);
    		sqlSnippets.add(currentSQLSnippet);
    	}

    	@Override
    	public SQLAppender catColumn(Selectable<?> column) {
    		// Columns are simply appended (no binder needed nor index increment)
    		currentSQLSnippet.catColumn(column);
    		return this;
    	}

    	@Override
    	public SQLAppender catTable(Fromable table) {
    		currentSQLSnippet.catTable(table);
    		return this;
    	}

    	@Override
    	public SQLAppender removeLastChars(int length) {
    		currentSQLSnippet.removeLastChars(length);
    		return this;
    	}

    	/**
    	 * Implementation based on {@link ParsedSQL#toString()}. Not really useful for a real SQL statement, prefer {@link #toPreparedSQL(Map)}
    	 *
    	 * @return a representation of internal SQL snippets
    	 */
    	@Override
    	public String getSQL() {
    		StringBuilder result = new StringBuilder();
    		visitSQLSnippets(placeholder ->
    				result.append(":").append(placeholder.getName()),
    				sqlAppender -> result.append(sqlAppender.getSQL()),
    				subSQLAppender -> result.append(subSQLAppender.getSQL()));
    		return result.toString();
    	}

    	private void visitSQLSnippets(Consumer<Placeholder<?, ?>> placeholderConsumer,
    								  Consumer<StringSQLAppender> sqlAppenderConsumer,
    								  Consumer<DefaultSubSQLAppender> subSQLAppenderConsumer) {
    		this.sqlSnippets.forEach(sqlSnippet -> {
    			if (sqlSnippet instanceof Placeholder) {
    				placeholderConsumer.accept((Placeholder<?, ?>) sqlSnippet);
    			} else if (sqlSnippet instanceof StringSQLAppender) {
    				sqlAppenderConsumer.accept((StringSQLAppender) sqlSnippet);
    			} else if (sqlSnippet instanceof DefaultSubSQLAppender) {
    				subSQLAppenderConsumer.accept((DefaultSubSQLAppender) sqlSnippet);
    			} else {
    				throw new IllegalStateException("Unsupported SQL snippet: "
    						+ (sqlSnippet == null ? "null" : Reflections.toString(sqlSnippet.getClass())));
    			}
    		});
    	}

    	@Override
    	public SubSQLAppender newSubPart(DMLNameProvider dmlNameProvider) {
    		SubSQLAppender result = new DefaultSubSQLAppender(
    				new ExpandableSQLAppender(
    						dmlNameProvider,
    						// we give all our attributes to make the subpart fill the global identifier counter and values, this avoids a complex
    						// computation at close() time to propagate the value to the parent
    						this.parameterBinderRegistry,
    						this.parameterBinders,
    						this.values,
    						this.paramCounter)) {
    			@Override
    			public SQLAppender close() {
    				// we ask for a new SQL snippet, it will reinitialize this.currentSQLSnippet and therefore prepare for another iteration as specified by newSubPart(..)
    				ExpandableSQLAppender.this.initCurrentSqlSnippet();
    				return ExpandableSQLAppender.this;
    			}
    		};
    		this.sqlSnippets.add(result);
    		return result;
    	}

    	/**
    	 * Creates a {@link PreparedSQL} from given values.
    	 * Given values are merged with those of current instance, hence only named placeholders values are required.
    	 * However, values present in original SQL can be overwritten by giving a value to their numeric placeholder.
    	 *
    	 * @param values values per named placeholder
    	 * @return a new {@link PreparedSQL} for given values set
    	 */
    	public PreparedSQL toPreparedSQL(Map<String, Object> values) {
    		Map<String, Object> mergedValues = new HashMap<>(getValues());
    		mergedValues.putAll(values);
    		// we unwrap Variables from values because PreparedSQL doesn't support it
    		unwrapVariables(mergedValues);

    		// we ask ExpandableSQL to build the SQL made of "?" for each of our values
    		Map<String, Integer> valuesSizes = ExpandableSQL.sizes(mergedValues);
    		ParsedSQL parsedSQL = new ParsedSQL();

    		class ParsedSQLHelper {

    			void add(ExpandableSQLAppender appender) {
    				appender.visitSQLSnippets(this::add,
    						this::add,
    						subSQLAppender -> add((ExpandableSQLAppender) subSQLAppender.getDelegate()));
    			}

    			void add(Placeholder variable) {
    				parsedSQL.addParam(variable.getName());
    			}

    			void add(StringSQLAppender sqlSnippet) {
    				parsedSQL.addSqlSnippet(sqlSnippet.getSQL());
    			}
    		}

    		new ParsedSQLHelper().add(this);

    		ExpandableSQL expandableSQL = new ExpandableSQL(parsedSQL, valuesSizes);
    		String placeholderSql = expandableSQL.getPreparedSQL();

    		// Computing parameter binders for each "?" index
    		Map<Integer, PreparedStatementWriter<?>> placeholderBinders = new HashMap<>();
    		Map<Integer, Object> placeholderValues = new HashMap<>();
    		expandableSQL.getExpandableParameters().forEach((paramName, expandableParameter) -> {
    			Object value = mergedValues.get(paramName);
    			if (value == null && !mergedValues.containsKey(paramName)) {
    				// value not given at all (null or not)
    				throw new BindingException("No value given for parameter named '" + paramName + "' : " + placeholderSql);
    			}
    			int[] markIndexes = expandableParameter.getMarkIndexes();
    			for (int markIndex : markIndexes) {
    				placeholderBinders.put(markIndex, parameterBinders.get(paramName));
    			}
    			ExpandableStatement.adaptIterablePlaceholders(value, markIndexes, placeholderValues::put);
    		});

    		PreparedSQL result = new PreparedSQL(placeholderSql, placeholderBinders);
    		result.setValues(placeholderValues);
    		return result;
    	}

    	/**
    	 * Replaces {@link ValuedVariable} values by their concrete values.
    	 * Note that this method replaces values of the given {@link Map} (for simplicity of calling algorithm)
    	 *
    	 * @param mergedValues {@link Map} in which {@link ValuedVariable} must be replaced by their own internal values
    	 */
    	private void unwrapVariables(Map<String, Object> mergedValues) {
    		mergedValues.entrySet().forEach(entry -> {
    			if (entry.getValue() instanceof ValuedVariable) {
    				entry.setValue(((ValuedVariable<?>) entry.getValue()).getValue());
    			}
    		});
    	}
    }